Skip to content

feat(status): surface full sdkKeys[]/mobileKeys[] arrays on /status#731

Merged
aaron-zeisler merged 1 commit into
feat/concurrent-keysfrom
aaronz/SDK-2534/status-endpoint-arrays
Jun 30, 2026
Merged

feat(status): surface full sdkKeys[]/mobileKeys[] arrays on /status#731
aaron-zeisler merged 1 commit into
feat/concurrent-keysfrom
aaronz/SDK-2534/status-endpoint-arrays

Conversation

@aaron-zeisler

@aaron-zeisler aaron-zeisler commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Note

Stacked PR — splitting #724 (final PR):

  1. fix(envfactory): reject primary mobile key absent from mobileKeys[] #728 — reject primary mobile key absent from mobileKeys[] ✅ merged
  2. refactor(credential): carry wire key identifiers through the accepted set #729 — carry wire key identifiers through the accepted set ✅ merged
  3. feat(credential): expose the full accepted key set via AcceptedKeys #730 — expose the full accepted key set via AcceptedKeys ✅ merged
  4. feat(status): surface full sdkKeys[]/mobileKeys[] arrays on /status #731 (this PR) — surface sdkKeys[] / mobileKeys[] on /status

Base: feat/concurrent-keys. Last PR of the stack.

Summary

Surface the full accepted credential set on /status as sdkKeys[] / mobileKeys[] arrays — each entry carrying the obscured value, the optional wire identifier, and an optional Unix-millisecond expiry. The existing scalar sdkKey / mobileKey fields are preserved and now designate which array entry is the anchor / primary.

expiringSdkKey is now computed deterministically from the accepted set: the previous code overwrote on each loop iteration (arbitrary with multiple expiring keys); it now reports the soonest-expiring non-anchor key, with a (expiry, value) tie-break.

Jira: SDK-2534

Background

The concurrent-keys epic (SDK-2453) lets an environment accept multiple SDK keys and mobile keys at once. Per the backend tech spec and RAC payload spec, the sdkKeys / mobileKeys arrays are the authoritative full accepted set, including the default/anchor key (also duplicated on the legacy scalar fields). The status endpoint mirrors that: the arrays carry every accepted key; the scalars designate the anchor / primary.

The wire format carries a non-secret key identifier alongside each credential's value (threaded through the model in #729 and exposed via AcceptedKeys() in #730). The identifier is genuinely optional — manual configuration and old-format (pre-concurrent-keys) payloads carry none — so it is modeled as a pointer and omitted from the JSON when absent.

Response schema

EnvironmentStatusRep gains two arrays plus a KeyStatus element type. The scalar fields are unchanged.

// KeyStatus is one accepted credential in the sdkKeys[] / mobileKeys[] arrays.
type KeyStatus struct {
    Key    string `json:"key,omitempty"`    // non-secret identifier; omitted when the source carried none
    Value  string `json:"value"`            // obscured credential secret
    Expiry *int64 `json:"expiry,omitempty"` // Unix-millis expiry; omitted for permanent keys
}

type EnvironmentStatusRep struct {
    SDKKey           string               `json:"sdkKey"`                   // obscured ANCHOR (designates which sdkKeys entry is the anchor)
    SDKKeys          []KeyStatus          `json:"sdkKeys"`                  // full server-key set, incl. anchor; always present, always ≥1
    EnvID            string               `json:"envId,omitempty"`
    EnvKey           string               `json:"envKey,omitempty"`
    EnvName          string               `json:"envName,omitempty"`
    ProjKey          string               `json:"projKey,omitempty"`
    ProjName         string               `json:"projName,omitempty"`
    MobileKey        string               `json:"mobileKey,omitempty"`      // obscured PRIMARY mobile key
    MobileKeys       []KeyStatus          `json:"mobileKeys"`               // full mobile-key set, incl. primary; always present, may be empty
    ExpiringSDKKey   string               `json:"expiringSdkKey,omitempty"` // soonest-expiring non-anchor SDK key (deterministic)
    Status           string               `json:"status"`
    ConnectionStatus ConnectionStatusRep  `json:"connectionStatus"`
    DataStoreStatus  DataStoreStatusRep   `json:"dataStoreStatus"`
    BigSegmentStatus *BigSegmentStatusRep `json:"bigSegmentStatus,omitempty"`
}

Array entry order is unspecified. The arrays are always present (never null): sdkKeys always contains at least the anchor; mobileKeys is empty for an environment with no mobile key (e.g. server-side only).

Example (environment mid-rotation: an extra service key + an expiring key)

"environment1": {
  "sdkKey": "sdk-********-****-****-****-*******00001",
  "sdkKeys": [
    { "key": "default",   "value": "sdk-********-****-****-****-*******00001" },
    { "key": "service-a", "value": "sdk-********-****-****-****-*******00002" },
    { "key": "old-key",   "value": "sdk-********-****-****-****-*******00003", "expiry": 1735689600000 }
  ],
  "mobileKey": "mob-********-****-****-****-*******00009",
  "mobileKeys": [
    { "key": "default-mobile", "value": "mob-********-****-****-****-*******00009" }
  ],
  "expiringSdkKey": "sdk-********-****-****-****-*******00003",
  "status": "connected",
  "connectionStatus": { "state": "VALID", "stateSince": 10000000 },
  "dataStoreStatus":  { "state": "VALID", "stateSince": 10000000 }
}

Changes

  • Add KeyStatus and the sdkKeys[] / mobileKeys[] fields to EnvironmentStatusRep.
  • The handler takes a single consistent GetAcceptedKeys() snapshot and drives every credential field from it — the scalar anchor/primary designations, the arrays (one per Server / Mobile group, always present), and expiringSdkKey. Because the anchor designation comes from the same snapshot as the key set, the two can't drift under a concurrent reconcile (previously the anchor was read separately).
  • expiringSdkKey is the soonest-expiring non-anchor SDK key via slices.MinFunc, comparing (expiry, value) so the pick is deterministic on a tie.

@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/rotator-accepted-keys branch from f063227 to 0635b12 Compare June 30, 2026 16:58
@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/status-endpoint-arrays branch from 9b4302f to ef93f46 Compare June 30, 2026 16:58
@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/rotator-accepted-keys branch from 0635b12 to 306ea66 Compare June 30, 2026 17:22
@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/status-endpoint-arrays branch from ef93f46 to 016dc3e Compare June 30, 2026 17:24

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 016dc3e. Configure here.

Comment thread relay/endpoints_status.go Outdated
@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/rotator-accepted-keys branch from 306ea66 to 0ef2308 Compare June 30, 2026 18:14
@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/status-endpoint-arrays branch from 016dc3e to 3316745 Compare June 30, 2026 18:17
@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/rotator-accepted-keys branch from 0ef2308 to f4e6263 Compare June 30, 2026 20:16
Base automatically changed from aaronz/SDK-2534/rotator-accepted-keys to feat/concurrent-keys June 30, 2026 20:25
Add a KeyStatus representation and sdkKeys[]/mobileKeys[] arrays to
EnvironmentStatusRep. The handler takes a single consistent snapshot via
GetAcceptedKeys() and drives every credential field from it — the scalar
anchor/primary designations, the full arrays (grouped by kind, always
present), and expiringSdkKey — so they cannot drift relative to each other
under a concurrent reconcile (previously the anchor was read separately
from the key set). Each entry carries the obscured value, the optional
wire identifier, and an optional Unix-millisecond expiry.

expiringSdkKey is the soonest-expiring non-anchor SDK key, with a
(expiry, value) tie-break so the pick is deterministic when several keys
share an expiry.
@aaron-zeisler aaron-zeisler force-pushed the aaronz/SDK-2534/status-endpoint-arrays branch from 3316745 to 449ca7d Compare June 30, 2026 20:28
@aaron-zeisler aaron-zeisler merged commit 9737404 into feat/concurrent-keys Jun 30, 2026
18 checks passed
@aaron-zeisler aaron-zeisler deleted the aaronz/SDK-2534/status-endpoint-arrays branch June 30, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants